#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <signal.h>
#include <netdb.h>
#include <sys/errno.h>
#include <sys/time.h>
#include <netinet/ip_icmp.h>
#include <netinet/ip.h>
#include <errno.h>

/* 常量定义区域 */
#define MAX_PACKETS 4        // 最大发送的ICMP请求包数量
#define PACKET_SIZE 4096     // 发送和接收缓冲区的大小
#define DATA_LEN 56          // ICMP数据部分的长度
#define MAX_ALARM_TIME 5     // 接收超时时间（秒）

/* 全局变量定义 */
struct sockaddr_in fromAddr = {0};  // 存储发送方地址信息
int nsend = 0;                      // 已发送包计数器
int nrecved = 0;                    // 已接收包计数器
struct timeval recvTimeVal;         // 记录包接收时间
char sendPacket[PACKET_SIZE];       // 发送包缓冲区
char recvPacket[PACKET_SIZE];       // 接收包缓冲区

// 函数声明
void send_packet(int fd, struct sockaddr_in* pDestAddr, int len);
int pack(int nsend);
u_short ckcsum(u_short* p_icmp, int pack_size);
void recv_packet(int sfd);
int unpack(int len, uid_t uid);
void sigalrmHand(int s);
void tv_sub(struct timeval* out, struct timeval* in);

/**
 * @brief 程序主函数
 * @param argc 命令行参数个数
 * @param argv 命令行参数数组，argv[1]为目标主机地址
 * @return int 程序退出状态码
 */
int main(int argc, char* argv[])
{
    // 检查参数数量，至少需要目标主机地址
    if (argc < 2)
    {
        printf("请输入目的主机ip 地址\n");
        exit(-1);
    }

    // 获取ICMP协议号
    struct protoent* protocal = getprotobyname("icmp");
    if (NULL == protocal)
    {
        printf("getprotobyname失败\n");
        exit(-1);
    }

    // 创建原始套接字，需要root权限
    int sfd = socket(AF_INET, SOCK_RAW, protocal->p_proto);
    if (-1 == sfd)
    {
        printf("创建socket失败\n");
        exit(-1);
    }
    printf("创建socket成功\n");

    // 设置套接字接收缓冲区大小
    int size = 1024 * 50;
    int r = setsockopt(sfd, SOL_SOCKET, SO_RCVBUF, &size, sizeof(size));
    if (-1 == r)
    {
        printf("设置socket失败\n");
        exit(-1);
    }
    printf("设置socket成功\n");

    // 解析目标地址
    struct sockaddr_in dest_addr = {0};
    dest_addr.sin_family = AF_INET;

    // 尝试将参数转换为IP地址
    in_addr_t iaddr = inet_addr(argv[1]);
    struct hostent* host = NULL;

    if (INADDR_NONE == iaddr)
    {
        // 如果不是有效的IP地址，尝试作为域名解析
        host = gethostbyname(argv[1]);
        if (NULL == host)
        {
            printf("错误的ip地址\n");
            exit(-1);
        }
        // 复制解析得到的IP地址
        memcpy(&(dest_addr.sin_addr), host->h_addr, host->h_length);
        printf("解析域名 %s 为 IP: %s\n", argv[1], inet_ntoa(dest_addr.sin_addr));
    }
    else
    {
        // 直接使用IP地址
        dest_addr.sin_addr.s_addr = iaddr;
    }

    // 发送ICMP请求包
    send_packet(sfd, &dest_addr, sizeof(dest_addr));

    // 接收ICMP回复包
    recv_packet(sfd);

    return 0;
}

/**
 * @brief 发送ICMP请求包
 * @param sfd 原始套接字描述符
 * @param pDestAddr 目标地址结构体指针
 * @param len 地址结构体长度
 */
void send_packet(int sfd, struct sockaddr_in* pDestAddr, int len)
{
    int packetSize;
    int r = 0;

    // 循环发送指定数量的包
    while (nsend < MAX_PACKETS)
    {
        nsend++;
        // 打包ICMP数据
        packetSize = pack(nsend);
        // 发送数据包
        r = sendto(sfd, sendPacket, packetSize, 0, (struct sockaddr*)pDestAddr, len);
        if (r < 0)
        {
            printf("第%d个包发送失败\n", nsend);
            continue;
        }
        printf("第%d个包发送成功\n", nsend);
        sleep(1);  // 每秒发送一个包
    }
}

/**
 * @brief 打包ICMP请求包
 * @param nsend 包序列号
 * @return int 打包后的包大小
 */
int pack(int seq)
{
    // 将发送缓冲区解释为ICMP头部结构
    struct icmp* pIcmp = (struct icmp*)sendPacket;

    // 设置ICMP类型为回显请求
    pIcmp->icmp_type = ICMP_ECHO;
    pIcmp->icmp_code = 0;

    // 设置标识符为当前进程ID，序列号为传入的参数
    pIcmp->icmp_id = getpid();  // 使用进程ID作为标识符
    pIcmp->icmp_seq = seq;

    // 计算包总大小（头部8字节+数据部分）
    int packSize = 8 + DATA_LEN;

    // 在数据部分存储当前时间，用于计算往返时间
    struct timeval* tval = (struct timeval*)pIcmp->icmp_data;
    gettimeofday(tval, NULL);

    // 计算ICMP校验和
    pIcmp->icmp_cksum = 0;  // 先将校验和字段清零
    pIcmp->icmp_cksum = ckcsum((u_short*)pIcmp, packSize);

    return packSize;
}

/**
 * @brief 计算ICMP校验和
 * @param p_icmp ICMP包指针
 * @param pack_size 包大小
 * @return u_short 计算得到的校验和
 */
u_short ckcsum(u_short* p_icmp, int pack_size)
{
    int nleft = pack_size;    // 剩余数据长度
    int sum = 0;              // 累加和
    u_short* w = p_icmp;      // 数据指针
    u_short ans = 0;          // 临时变量

    // 将数据按16位分组累加
    while (nleft > 1)
    {
        sum += *w++;
        nleft -= 2;
    }

    // 处理奇数长度情况的最后一个字节
    if (1 == nleft)
    {
        *(unsigned char*)(&ans) = *(unsigned char*)w;
        sum += ans;
    }

    // 将高16位与低16位相加，并处理进位
    sum = (sum >> 16) + (sum & 0xffff);
    sum += (sum >> 16);

    // 取反得到校验和
    ans = ~sum;
    return ans;
}

/**
 * @brief 接收ICMP回复包
 * @param sfd 原始套接字描述符
 */
void recv_packet(int sfd)
{
    int r;
    socklen_t recvLen = sizeof(fromAddr);  // 地址结构体长度

    // 设置超时信号处理函数
    signal(SIGALRM, sigalrmHand);

    // 循环接收回复包，直到收到所有发送包的回复
    while (nrecved < nsend)
    {
        // 设置接收超时
        alarm(MAX_ALARM_TIME);

        // 接收数据包
        r = recvfrom(sfd, recvPacket, PACKET_SIZE - 1, 0,
                    (struct sockaddr*)&fromAddr, &recvLen);
        if (r < 0)
        {
            // 如果是信号中断导致的错误，继续接收
            if (EINTR == errno)
            {
                continue;
            }
            printf("接收失败:%m\n");
            continue;
        }

        // 记录接收时间
        gettimeofday(&recvTimeVal, NULL);

        // 解包并处理接收到的ICMP包
        if (-1 == unpack(r, getpid()))
        {
            continue;  // 如果不是我们的包，继续接收
        }

        nrecved++;  // 成功接收包计数
    }
}

/**
 * @brief 超时信号处理函数
 * @param s 信号编号
 */
void sigalrmHand(int s)
{
    // 计算丢包率
    float loss_rate = ((nsend - nrecved) * 1.0 / nsend * 100);

    printf("-----------------ping-----------------\n");
    printf("%d个包发送成功 %d个包接收成功 %%%f丢失\n",
           nsend, nrecved, loss_rate);
    printf("-----------------ping-----------------\n");
    exit(-1);
}

/**
 * @brief 解包和处理接收到的ICMP包
 * @param len 接收到的包长度
 * @param uid 期望的进程ID（用于过滤不属于本程序的回复）
 * @return int 成功返回0，失败返回-1
 */
int unpack(int len, uid_t uid)
{
    // 获取IP头部
    struct ip* ip = (struct ip*)recvPacket;

    // 计算IP头部长度（以字节为单位）
    int iphdrlen = ip->ip_hl << 2;

    // 跳过IP头部，定位到ICMP头部
    char* buf = recvPacket;
    struct icmp* icmp = (struct icmp*)(buf + iphdrlen);  // 修正指针计算
    len -= iphdrlen;  // 减去IP头部长度得到ICMP部分长度

    // 检查ICMP包长度是否足够
    if (len < 8)
    {
        printf("ICMP包大小小于8字节\n");
        return -1;
    }

    struct timeval* tvsend;

    // 检查是否是我们的回显回复包
    if ((ICMP_ECHOREPLY == icmp->icmp_type) && (uid == icmp->icmp_id))
    {
        // 获取发送时记录的时间
        tvsend = (struct timeval*)icmp->icmp_data;

        // 计算往返时间
        tv_sub(&recvTimeVal, tvsend);

        // 计算往返时间（毫秒）
        long rtt_ms = recvTimeVal.tv_sec * 1000 + recvTimeVal.tv_usec / 1000;

        printf("%d byte from %s: icmp_seq=%u ttl=%d rtt=%.3f ms\n",
               len, inet_ntoa(fromAddr.sin_addr), icmp->icmp_seq,
               ip->ip_ttl, rtt_ms / 1000.0);
    }
    else
    {
        return -1;  // 不是我们的包
    }
    return 0;
}

/**
 * @brief 计算两个时间值的差
 * @param out 输出参数，存储时间差
 * @param in 输入参数，被减的时间值
 */
void tv_sub(struct timeval* out, struct timeval* in)
{
    // 先减微秒部分，如果需要借位则从秒部分借
    if ((out->tv_usec -= in->tv_usec) < 0)
    {
        --out->tv_sec;
        out->tv_usec += 1000000;
    }
    out->tv_sec -= in->tv_sec;
}